/** Resizeable UI drawn with Cairo.
 */

#include <time.h>  // time(), time_t for TimeDisplay
#include <sys/time.h>  // gettimeofday() for mcp timer
#include <cairo.h>

//#ifdef USE_XLIB
//#include <sys/param.h>  // endian-ness for initAppIcon
//#include <X11/keysymdef.h>
//#endif

#include "mcp.h"
#include "mediainfo.h"
#include "coverart.h"

#include "ui_cairo.h"
#include "uic_widget.h"
#ifdef CAIRO_XLIB
#include "uic_xlib.h"
#elif defined(CAIRO_XCB)
#include "uic_xcb.h"
#endif

UI *new_cairoui () { return new uic::CairoUI(); }

namespace uic
{

//    Color object implementation                                         >fold>

void Color::set (cairo_t *cr)
    {
//    if ( alpha != 1 )
        cairo_set_source_rgba(cr, red, green, blue, alpha);
//    else
//        cairo_set_source_rgb(cr, red, green, blue);
    }

// These are some basic dirt-simple brightness/darkness adjustments.

// delta must be between -1 and 1.
Color *Color::adjust (double delta)
    {
    if ( delta > 0 )
        {
        red = delta + red * (1-delta);
        green = delta + green * (1-delta);
        blue = delta + blue * (1-delta);
        }
    else if ( delta < 0 )
        {
        delta = 1.0 + delta;
        red = red * delta;
        green = green * delta;
        blue = blue * delta;
        }
    else
        ;  // quit wasting my time!
    return this;
    }

void Color::set_adjusted (cairo_t *cr, double delta)
    {
    if ( delta > 0 )
        {
        // color + (1-color) * delta  =  a + c(1-a)
        cairo_set_source_rgba(cr, delta + red * (1.0-delta),
                                     delta + green * (1.0-delta),
                                     delta + blue * (1.0-delta), alpha);
        }
    else if ( delta < 0 )
        {
        // color - color * delta  =  c(1 - a)
        delta = 1.0 + delta;
        cairo_set_source_rgba(cr,
                              red * delta, green * delta, blue * delta, alpha);
        }
    else
        set(cr);
    }

// ---------------------------------------------------------------------- <fold<
//    Widget Layout                                                       >fold>

#ifdef UIC_CUSTOM_LAYOUT 
bool layout_widgets(CairoUI *ui, bool no_resize);  // uic_layout.cpp
#endif

void CairoUI::update_layout ()
    {
#ifdef UIC_CUSTOM_LAYOUT 
    if ( !(layout_widgets(this, false)) )
#endif
        assert(0);
        //layout_widgets_internal();
    }

/*  Basic Window Layout Goal:
 *
 *   -------------   B  buttons  (stop, play/pause, volume/mute)
 *   | B B B  NN |   N  playlist index (track number)
 *   | --------- |   T  time display (time remaining, time to go, etc)
 *   |    TTT    |   -  progress bar for current track
 *   -------------  
 */

/* The layout function below is a hand-coded one, useful for a few reasons:
 * 1) useful default operation without a configuration file
 *    (or without custom layout compiled in)
 * 2) aims to be useful as small as 80x50
 *
 * TODO  Implement a useful layout for 64x64 and maybe 32x32. (dock/icon sizes)
 *        -   3 buttons + position text doesn't quite fit
 *            -  possibly put the 3 buttons vertically on the left side
 *        -   need the time display to only show hh:mm if there is not room
 *            for hh:mm:ss
 */

/*  3 16px buttons, 2 10px numbers & 1 px between, 4 2px spacing, 3 px spacing
    = 80 width, * phi for 49.4 height
 */
#define MIN_SIZE_X 80
#define MIN_SIZE_Y 50

#if 0
 not yet upgraded for the widget list  (no widget type now..)

void CairoUI::layout_widgets_internal ()
    {
    bool vertical_layout;
    int  effwidth = width,    // effective width and height
         effheight = height;  // (compensates for the cover art widget)
    int  effleft = 0,
         efftop = 0;

    #define set_dim2 (w) { w->x2 = w->x1 + w->width; \
                           w->y2 = w->y1 + w->height; }

    if ( mcp->connected() )
        {
        if ( !widget[WT_STOP] )
            {
            widget[WT_STOP] = &(w->stop);
            widget[WT_PLAYPAUSE] = &(w->playpause);
            widget[WT_VOLUME] = &(w->volume);
            widget[WT_POSITION] = &(w->position);
            widget[WT_PROGRESSBAR] = &(w->pbar);
            widget[WT_TIMEDISPLAY] = &(w->timedisplay);
            widget[WT_ICON] = 0;
            }
        if ( mcp->stopped() )
            {
            widget[WT_PROGRESSBAR] = 0;
            widget[WT_TIMEDISPLAY] = 0;
            widget[WT_ICON] = &(w->icon);
            }
        else  // playing or paused
            {
            if ( !widget[WT_PROGRESSBAR] )
                {
                widget[WT_PROGRESSBAR] = &(w->pbar);
                widget[WT_TIMEDISPLAY] = &(w->timedisplay);
                widget[WT_ICON] = 0;
                }
            }
        }
    else  // disconnected
        {
        if ( widget[WT_STOP] )
            {
            widget[WT_STOP] = 0;
            widget[WT_PLAYPAUSE] = 0;
            widget[WT_VOLUME] = 0;
            widget[WT_POSITION] = 0;
            widget[WT_PROGRESSBAR] = 0;
            widget[WT_TIMEDISPLAY] = 0;
            widget[WT_ICON] = &(w->icon);
            }
        }

    //
    // for x :  base_size/base_max  to  new_size/new_width
    // therefore :  new_size = new_width * base_size / base_max
    #define XUNIT(base_size) (effwidth  * (base_size) / MIN_SIZE_X)
    #define YUNIT(base_size) (effheight * (base_size) / MIN_SIZE_Y)
    #define BUTTON_WIDTH     XUNIT(16)
    #define BUTTON_HEIGHT    YUNIT(16)

#ifdef COVERART
    if ( prefs->is_set("coverart") )
        {
        int min_ca_size = w->coverart.min_size;
        widget[WT_COVERART] = 0;
        if ( width >= min_ca_size && height >= min_ca_size )  
            {
            if ( width > height + min_ca_size )
                {
                widget[WT_COVERART] = &(w->coverart);
                w->coverart.width = height;
                w->coverart.height = height;

                w->coverart.check_size(this);
                effwidth = width - w->coverart.width;
//                if ( !w->coverart.layout_right )
                    effleft = w->coverart.width;

//                w->coverart.x1 = w->coverart.layout_right ? effwidth : 0;
                w->coverart.x1 = 0;
                w->coverart.y1 = 0;
                w->coverart.set_dim2();
                w->coverart.configure(this);
                }
            else if ( height > width + min_ca_size )
                {
                widget[WT_COVERART] = &(w->coverart);
                w->coverart.width = width;
                w->coverart.height = width;

                w->coverart.check_size(this);
                effheight = height - w->coverart.height;
//                if ( !w->coverart.layout_top )
                    efftop = w->coverart.height;

                w->coverart.x1 = 0;
//                w->coverart.y1 = w->coverart.layout_top ? 0 : effheight;
                w->coverart.y1 = 0;
                w->coverart.set_dim2();
                w->coverart.configure(this);
                }
            }
        }
#endif

    vertical_layout = ( effheight > effwidth * 1.75 );

    border_size = YUNIT(1);
    if ( border_size > XUNIT(1) )
        border_size = XUNIT(1);
    if ( border_size > 5 )  // FIXME preference
        border_size = 5;

    if ( widget[WT_STOP] )
        {
        if ( vertical_layout )
            w->stop.x1 = effleft + effwidth/2 - BUTTON_WIDTH/2 - BUTTON_WIDTH;
        else
            w->stop.x1 = effleft + XUNIT(1);
        w->stop.y1 = efftop + YUNIT(1);
        w->stop.width = BUTTON_WIDTH;
        w->stop.height = BUTTON_HEIGHT;
        w->stop.check_size(this);
        w->stop.set_dim2();
        w->stop.configure(this);
        }

    if ( widget[WT_PLAYPAUSE] )
        {
        w->playpause.x1 = w->stop.x2;
        w->playpause.y1 = w->stop.y1;
        w->playpause.width = BUTTON_WIDTH;
        w->playpause.height = BUTTON_HEIGHT;
        w->playpause.check_size(this);
        w->playpause.set_dim2();
        w->playpause.configure(this);
        }

    if ( widget[WT_VOLUME] )
        {
        w->volume.x1 = w->playpause.x2;
        w->volume.y1 = w->stop.y1;
        w->volume.width = BUTTON_WIDTH;
        w->volume.height = BUTTON_HEIGHT;
        w->volume.check_size(this);
        w->volume.set_dim2();
        w->volume.configure(this);
        }

    if ( vertical_layout )
        {
        if ( widget[WT_POSITION] )
            {
            w->position.x1 = effleft + XUNIT(1);
            w->position.y1 = w->stop.y2 + YUNIT(1);
            w->position.width = effwidth - XUNIT(2) - 2*border_size;
            w->position.height = w->stop.height - YUNIT(2);
            w->position.set_justify(JUSTIFY_CENTER);
            }
        if ( widget[WT_TIMEDISPLAY] )
            {
            w->timedisplay.x1 = effleft + XUNIT(1);
            w->timedisplay.y1 = w->position.y2 + YUNIT(1);
            w->timedisplay.width = effwidth - XUNIT(2) - 2*border_size;
            w->timedisplay.height = w->stop.height - YUNIT(2);
            }
        if ( widget[WT_PROGRESSBAR] )
            {
            w->pbar.y1 = w->timedisplay.y2 + YUNIT(1) + border_size;
            w->pbar.height = effheight - (w->pbar.y1 - efftop) \
                             - YUNIT(2) - 2*border_size;
            w->pbar.width = XUNIT(16);
            w->pbar.x1 = effleft + (effwidth / 2) - (w->pbar.width / 2);
            w->pbar.vertical = true;
            }
        }
    else  //  normal layout:
        {
        if ( widget[WT_POSITION] )
            {
            w->position.width = effwidth - (w->position.x1 - effleft) - XUNIT(2);
            w->position.height = w->stop.height - YUNIT(2);
            w->position.x1 = w->volume.x2 + XUNIT(1);
            w->position.y1 = w->stop.y1 + w->stop.height/2 -
                             w->position.height/2 -1;
            w->position.set_justify(JUSTIFY_RIGHT);
            }
        if ( widget[WT_TIMEDISPLAY] )
            {
            w->timedisplay.x1 = effleft + XUNIT(1);
            w->timedisplay.width = effwidth - XUNIT(2) - 2*border_size;
            w->timedisplay.height = w->stop.height - YUNIT(2);
            w->timedisplay.y1 = efftop + effheight \
                               - YUNIT(3) - w->timedisplay.height;
            }
        if ( widget[WT_PROGRESSBAR] )
            {
            w->pbar.x1 = effleft + XUNIT(1);
            w->pbar.width = effwidth - XUNIT(2) - 2*border_size;
            w->pbar.height = YUNIT(9);
            w->pbar.y1 = efftop + (effheight / 2) - (w->pbar.height / 2);
            w->pbar.vertical = false;
            }
        }
    w->position.check_size(this);
    w->timedisplay.check_size(this);
    w->pbar.check_size(this);

    w->position.set_dim2();
    w->timedisplay.set_dim2();
    w->pbar.set_dim2();

    w->position.configure(this);
    w->timedisplay.configure(this);
    w->pbar.configure(this);

    if ( widget[WT_ICON] )
        {
        if ( widget[WT_STOP] )  // assuming the progress bar/timedisplay are
            {                  // not enabled
            w->icon.height = effheight - w->playpause.y2 - YUNIT(2);
            w->icon.y1 = w->playpause.y2 + YUNIT(1);
            }
        else  // disconnected entirely, icon takes the whole area
            {
            w->icon.height = height - YUNIT(2);
            w->icon.y1 = YUNIT(1);
            }
        if ( w->icon.height > (effwidth - XUNIT(2)) )
            {
            // center the icon vertically if the height is reduced
            w->icon.y1 += (w->icon.height - (effwidth - XUNIT(2))) / 2;
            w->icon.height = (effwidth - XUNIT(2));
            }
        w->icon.width = w->icon.height;
        w->icon.check_size(this);
        w->icon.x1 = effleft + (effwidth - w->icon.width) / 2;
        w->icon.set_dim2();
        w->icon.configure(this);
        }

    #undef XUNIT
    #undef YUNIT
    #undef BUTTON_WIDTH
    #undef BUTTON_HEIGHT
    }
#endif

// ---------------------------------------------------------------------- <fold<
//    CairoUI object                                                      >fold>

Preferences *new_preferences_object();  // preferences.cpp

CairoUI::CairoUI ()
    {
    label = "Cairo";
    default_window_title = "rmc";
    timer_activate.tv_sec = 0;
    timer_activate.tv_usec = 0;
    focused = 0;

    widget_list = 0;
    widget_pool = 0;

    release_pending[LEFT_CLICK] = 0;
    release_pending[RIGHT_CLICK] = 0;
    release_pending[MIDDLE_CLICK] = 0;

    need_flush = false;

    // CairoUI preferences:
    prefs = Preferences::default_type();
//    prefs = default_preferences();
    prefs->set_default("coverart", "true");

#ifdef CAIRO_XLIB
    local = new UIC_Xlib();
#elif defined(CAIRO_XCB)
    local = new UIC_Xcb();
#endif
    assert(local);
    }

CairoUI::~CairoUI ()
    {
    Widget *next;
    while ( widget_list )
        {
        next = widget_list->next;
        free(widget_list);
        widget_list = next;
        }
    while ( widget_pool )
        {
        next = widget_pool->next;
        free(widget_pool);
        widget_pool = next;
        }
    if ( fd ) delete fd;
    }

void CairoUI::fd_exception (int fd)
    {
    local->fd_exception(this, fd);
    }

// FIXME  This is kind of dumb.
void CairoUI::set_song_title ()
    {
    if ( mcp->current_info )
        {
        char buf[555];
        if ( mcp->current_info->artist.length() > 0 )
             {
             sprintf(buf, "%1.400s / %1.100s - %1.45s", mcp->current_info->title.c_str(), mcp->current_info->artist.c_str(), default_window_title);
             }
        else if ( mcp->current_info->title.length() > 0 )
            {
            sprintf(buf, "%1.500s - %s", mcp->current_info->title.c_str(), default_window_title);
            }
        else
            strcpy(buf, default_window_title);
        local->set_title(buf);
        }
    }

void CairoUI::perform_flush ()
    {
    if ( need_flush )
        {
        local->perform_flush(this);
        need_flush = false;
        }
    }

// ---------------------------------------------------------------------- <fold<
//    Configuration                                                       >fold>

ConfigProcessor *CairoUI::init_processor ()
    { return prefs->processor("UI.CAIRO"); }

ConfigProcessor *uic_layout_processor(CairoUI *ui);  // uic_layout.cpp
ConfigProcessor *CairoUI::processor ()
    { return uic_layout_processor(this); }

void CairoUI::apply_prefs (void)
    {
    if ( !get_color_pref(&default_fg, "color.foreground") )
        {
        // default chosen by the long, detailed study of a focus group of 1
        default_fg.red   = 164.0/255; default_fg.green = 125.0/255;
        default_fg.blue  = 189.0/255; default_fg.alpha = 1.0;
        }
    if ( !get_color_pref(&default_bg, "color.background") )
        {
        default_bg.red   = 0/255; default_bg.green = 10/255;
        default_bg.blue  = 0/255; default_bg.alpha = 1.0;
        }

    if ( !get_color_pref(&text_fg, "color.text") )
        text_fg = default_fg;
    if ( !get_color_pref(&pbar_fg, "color.progress bar") )
        pbar_fg = default_fg;

    if ( !get_color_pref(&button_fg, "color.button foreground") )
        button_fg = default_fg;
    if ( !get_color_pref(&button_fg1, "color.button foreground1") )
        {
        button_fg1 = button_fg;
        button_fg1.adjust(0.4);
        }
    if ( !get_color_pref(&button_fg2, "color.button foreground2") )
        {
        button_fg2 = button_fg;
        button_fg2.adjust(-0.3);
        }

    if ( !get_color_pref(&button_bg, "color.button background") )
        {
        // chosen to match the gray color of soul-crushing despair
        button_bg.red = .2;  button_bg.green = .2;
        button_bg.blue = .2; button_bg.alpha = 1;
        }
    //if ( !get_color_pref(&button_high, "color.button highlight") )
    //if ( !get_color_pref(&button_shadow, "color.button shadow") )

    if ( !get_color_pref(&icon_fg, "color.icon foreground") )
        {
        // white border for the icon
        icon_fg.red   = 255.0/255; icon_fg.green = 255.0/255;
        icon_fg.blue  = 255.0/255; icon_fg.alpha = 1.0;
        }
    if ( !get_color_pref(&icon_bg, "color.icon background") )
        {
        // approximate color match for the Official Logo
        icon_bg.red   =  10.0/255; icon_bg.green =  92.0/255;
        icon_bg.blue  = 128.0/255; icon_bg.alpha = 1.0;
        }

    }

bool CairoUI::get_color_pref (Color *color, const char *prefname)
    {
    const char *str = prefs->get(prefname);
    if ( !str )
        return false;

    {  // direct-to-cairo, 0.0 to 1.0
    double r, g, b, a = 1.0;
    if ( strstr(str, ".") )  // dumb but good enough for now
        {
        if ( sscanf(str, "%lf %lf %lf %lf", &r, &g, &b, &a) == 4
             || sscanf(str, "%lf %lf %lf", &r, &g, &b) == 3
#ifndef CAIRO_XLIB
             || sscanf(str, "rgbi:<%lf/%lf/%lf>", &r, &g, &b) == 3
#endif
           )
            {
            color->red = r; color->green = g; color->blue = b; color->alpha = a;
            return true;
            }
        }
    }

    {  // simple version, 0 to 255
    int r, g, b, a = 255;
    if ( sscanf(str, "%d %d %d %d", &r, &g, &b, &a) == 4
         || sscanf(str, "%d %d %d", &r, &g, &b) == 3
         // < insert other 0 to 255 format here >
       )
        {
        color->red  = (double)r/255.0; color->green = (double)g/255.0;
        color->blue = (double)b/255.0; color->alpha = (double)a/255.0;
        return true;
        }
    }

#ifdef CAIRO_XLIB
    {
    if ( ((UIC_Xlib *)local)->parse_color(str, color) )
        return true;
    }
#endif
    return false;
    }

// ---------------------------------------------------------------------- <fold<
//    -    Timing, Timer, etc.                                            >fold>

static bool tv_is_less (struct timeval *tv1, struct timeval *tv2)
    {
    if ( tv1->tv_sec < tv2->tv_sec )
        return true;
    else if ( tv1->tv_sec == tv2->tv_sec && tv1->tv_usec < tv2->tv_usec )
        return true;
    return false;
    }

// FIXME To be safe this should check for sizeof long being exceeded
//       -  also, in general this seems kind of dumb..
static unsigned long tv_diff (struct timeval *tv1, struct timeval *tv2)
    {
    unsigned long result;
    if ( tv1->tv_sec == tv2->tv_sec )
        {
        if ( tv1->tv_usec < tv2->tv_usec )
            result = tv2->tv_usec - tv1->tv_usec;
        else
            result = tv1->tv_usec - tv2->tv_usec;
        }
    else
        {
        if ( tv1->tv_sec > tv2->tv_sec )
            { struct timeval *hold = tv1; tv1 = tv2; tv2 = hold; }
        result = (tv2->tv_sec - tv1->tv_sec) * 100000;
        result += 100000 - tv1->tv_usec;
        result += tv2->tv_usec;
        }
    return result;
    }

bool CairoUI::timer_wanted ()
    {
    struct timeval now;
    if ( gettimeofday(&now, 0) == -1 )
        return false;

    if ( !tv_is_less(&timer_activate, &now) )
        return true;

    Widget *o;
    for ( o = widget_list; o; o = o->next )
        if ( o->redraw_needed ||
             (o->timer_activate.tv_sec != 0 || o->timer_activate.tv_usec != 0) )
            return true;

    return false;
    }

void CairoUI::timer ()
    {
    struct timeval now;
    struct timeval then;
    gettimeofday(&now, 0);

    if ( !tv_is_less(&timer_activate, &now) )
        timer_event();

    for ( Widget *o = widget_list; o; o = o->next )
        if ( o->timer_activate.tv_sec != 0 && o->timer_activate.tv_usec != 0 &&
             !tv_is_less(&now, &o->timer_activate) )
            {
            o->timer_event(this);
            o->timer_activate.tv_sec = 0;
            o->timer_activate.tv_usec = 0;
            }

    for ( Widget *o = widget_list; o; o = o->next )
        if ( o->redraw_needed )
            {
    cairo_t *cr = local->get_cairo(this);
            if ( !draw_widget(cr, o) )
;//                cr = local->get_cairo(this);
    cairo_destroy(cr);
            need_flush = true;
            gettimeofday(&then, 0);
            if ( tv_diff(&now, &then) > 10000 )  // 1/10th sec, draw more later
                break;
            }

    perform_flush();
    }

void CairoUI::timer_event ()
    {
    }

// Centralize redrawing here
void CairoUI::post_event_cleanup ()
    {
    bool redraw_everything = false;

    {
    int count = 0, total = 0;
    for ( Widget *o = widget_list; o; o = o->next, total++ )
        if ( o->redraw_needed )
            count++;
    if ( count > total * 7 / 10 )  /// TODO  some smart choice with
        redraw_everything = true;  //        the total area to redraw
    }

    if ( redraw_everything )
        redraw(true);            
    else
        {
        for ( Widget *o = widget_list; o; o = o->next )
            if ( o->redraw_needed && !o->slow_redraw() )
                {
        cairo_t *cr = local->get_cairo(this);
//                mcp->log_verbose(11, "redrawing widget %d\n", o->layout_index);
//                cairo_save(cr);
                if ( draw_widget(cr, o) )
//                    cairo_restore(cr);
 //               else
  //                  cr = local->get_cairo(this);
                need_flush = true;
        cairo_destroy(cr);
                }

        perform_flush();
        }
    }
// ---------------------------------------------------------------------- <fold<
//    -    Widget handling                                                >fold>

void CairoUI::add_widget_to_list (Widget **list, Widget *widget)
    {
    if ( *list == 0 )
        {
        *list = widget;
        widget->prev = 0;
        }
    else
        {
        Widget *o = *list;
        while ( o->next ) o = o->next;
        o->next = widget;
        widget->prev = o;
        }
    widget->next = 0;
    }

void CairoUI::remove_widget_from_list (Widget **list, Widget *widget)
    {
    if ( *list == widget )
        {
        *list = widget->next;
        if ( *list != 0 )
            (*list)->prev = 0;
        widget->next = 0;
        widget->prev = 0;
        return;
        }
    for ( Widget *o = *list; o; o = o->next )
        if ( o->next == widget )
            {
            o->next = o->next->next;
            if ( o->next != 0 )
                o->next->prev = o;
            widget->next = 0;
            widget->prev = 0;
            return;
            }
    assert(2 + 2 == 5);
    }

#if 0
Widget * CairoUI::get_widget (WidgetType type, Widget *last)
    {
    if ( !last )
        last = widget_list;
    for ( ;; )
        ;
    switch ( type )
        {
        case WT_STOP:
            return &w->stop;
        case WT_PLAY:
            return &w->play;
        case WT_PAUSE:
            return &w->pause;
        case WT_PLAYPAUSE:
            return &w->playpause;
        case WT_PREVIOUS:
            return &w->previous;
        case WT_NEXT:
            return &w->next;
        case WT_VOLUME:
            return &w->volume;
        case WT_POSITION:
            return &w->position;
        case WT_TIMEDISPLAY:
            return &w->timedisplay;
        case WT_PROGRESSBAR:
            return &w->pbar;
        case WT_ICON:
            return &w->icon;
        case WT_COVERART:
#ifdef COVERART
            return &w->coverart;
#endif
        case WT_VISUAL:
            return 0; // &w->visual;

        default:
            assert(1);
            return 0;
        }
    }
#endif

void CairoUI::draw_background (cairo_t *cr)
    {
//mcp->log_verbose(1, "skipped draw_background\n");
//return;
    if ( default_bg.alpha < 1.0 )
        {
        cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
        cairo_paint(cr);
        }
    if ( default_bg.alpha > 0.0 )
        {
        cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
        default_bg.set(cr);
        cairo_paint(cr);
        }
    cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
    for ( Widget *o = widget_list; o; o = o->next )
        {
        cairo_save(cr);
        o->draw_background(this, cr, 0,0, width,height);
        cairo_restore(cr);
        }
    }

bool CairoUI::draw_widget (cairo_t *cr, Widget *w)
    {
    cairo_set_line_width(cr, 0);
    cairo_rectangle(cr, w->x1, w->y1, w->width, w->height);
//mcp->log_verbose(2, "clipping(%d)  %lf,%lf %lf,%lf\n", w->layout_index,
//            w->x1, w->y1, w->width, w->height);
    cairo_clip(cr);  // no need to save before this, done by callers

    if ( default_bg.alpha < 1.0 )
        {
        cairo_set_source_rgba(cr, 0,0,0,1);
        cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
        cairo_paint(cr);
        }
    cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
    if ( default_bg.alpha > 0.0 )
        {
        default_bg.set(cr);
        cairo_paint(cr);
        }

    cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
    for ( Widget *o = widget_list; o; o = o->next )
        if ( o != w )
            o->draw_background(this, cr, w->x1, w->y1, w->x2, w->y2);

    w->draw(this, cr);

    cairo_status_t stat = cairo_status(cr);
    if ( stat != CAIRO_STATUS_SUCCESS )
        {
        mcp->log_verbose(5, "cairo: (widget %d): %s\n",
                         w->layout_index, cairo_status_to_string(stat));
        return false;
        }
    return true;
    }

// redraw() clears the window and sets each widget as needing a repaint
// redraw(true) clears the window and immediately redraws all widgets
//              before returning, which may be slow..
void CairoUI::redraw (bool immediate)
    {
mcp->log_verbose(12, "CairoUI.redraw()\n");
    bool show_outlines = false;
    cairo_t *cr = 0;

    if ( immediate )
        {
        cr = local->get_cairo(this);
        draw_background(cr);
        }

    if ( !immediate && prefs->is_set("show outlines") )
        {
        int area;
        if ( prefs->is_true("show outlines") )
            show_outlines = true;
        else if ( prefs->get_int("show outlines", &area) )
            show_outlines = (x * y > area);
        else // if ( prefs->is_false("show outlines") )
            show_outlines = false;
        }
show_outlines=false;

    if ( show_outlines )
        {
        cr = local->get_cairo(this);
        cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
        cairo_set_line_width(cr, .5);
        cairo_set_source_rgb(cr, .5, .5, .5);
        for ( Widget *o = widget_list; o; o = o->next )
            if ( o->is_physical() )
                cairo_rectangle(cr, o->x1 + .5, o->y1 + .5,
                                    o->width-1, o->height-1);
        cairo_stroke(cr);
        }

    for ( Widget *o = widget_list; o; o = o->next )
        {
        if ( immediate && !o->slow_redraw() )
            {
            cairo_save(cr);
            cairo_status_t stat;
            o->draw(this, cr);
            stat = cairo_status(cr);
            cairo_restore(cr);
            if ( stat != CAIRO_STATUS_SUCCESS )
                {
                mcp->log_verbose(5, "cairo: (widget %i): %s\n",
                                 o->layout_index, cairo_status_to_string(stat));
                cr = local->get_cairo(this);
                }
            need_flush = true;
            }
        else
            o->redraw_needed = true;
        }

    if ( cr != 0 )
        {
        cairo_destroy(cr);
        perform_flush();
        }
    }

bool CairoUI::find_widget (int x, int y, Widget **widget_return)
    {
    for ( int run = 0; run < 2; run++ )
        for ( Widget *o = widget_list; o; o = o->next )
            if ( (run == 0 && o->is_physical()) ||
                 (run == 1 && !o->is_physical()) )
                if ( x >= o->x1 && x <= o->x1 + o->width &&
                     y >= o->y1 && y <= o->y1 + o->height )
                    {
                    if ( widget_return ) *widget_return = o;
                    return true;
                    }
    return false;
    }

bool CairoUI::in_widget (Widget *wi, int x, int y)
    {
    return ( wi && x >= wi->x1 && x <= wi->x2 &&
                   y >= wi->y1 && y <= wi->y2 );
    }

void CairoUI::button_press (GenericEvent *ev)
    {
mcp->log_verbose(12, "button press %d (%d,%d)\n", ev->button, ev->x, ev->y);
    Widget *o;
    bool handled = false;
    if ( find_widget(ev->x, ev->y, &o) )
        {
        if ( ev->button == LEFT_CLICK || ev->button == RIGHT_CLICK ||
             ev->button == MIDDLE_CLICK )
            {
            if ( release_pending[ev->button] )
                {
                fprintf(stderr, "widget still clicked OOPS\n");
                // wrong event, but probably better than nothing
                release_pending[ev->button]->button_release(this, ev, false);
                }
            release_pending[ev->button] = o;
            }
        handled = o->button_press(this, ev);
        }
    if ( !handled )
        {
        // <default button press event>
        }
    }

void CairoUI::button_release (GenericEvent *ev)
    {
mcp->log_verbose(12, "button release %d (%d,%d)\n",
                 ev->button, ev->x, ev->y);
    if ( ev->button == LEFT_CLICK || ev->button == RIGHT_CLICK ||
         ev->button == MIDDLE_CLICK )
        {
        if ( release_pending[ev->button] )
            {
            release_pending[ev->button]->button_release(this, ev,
                    in_widget(release_pending[ev->button], ev->x, ev->y));
            release_pending[ev->button] = 0;
            return;
            }
        }
    if ( ev->button >= WHEEL_UP && ev->button <= WHEEL_DOWN )
        {
        Widget *o;
        bool consumed = false;
        if ( find_widget(ev->x, ev->y, &o) )
            consumed = o->button_release(this, ev, false);
        if ( !consumed )
            {
            // <default button release event>
            if ( ev->button == WHEEL_UP )
                mcp->command(COMMAND_PREVIOUS);
            else if ( ev->button == WHEEL_DOWN )
                mcp->command(COMMAND_NEXT);
            }
        }
    }

void CairoUI::motion (GenericEvent *ev)
    {
    Widget *o;
    if ( find_widget(ev->x, ev->y, &o) )
        {
        if ( focused && focused != o )
            {
            focused->pointer_leave(this);
            focused = 0;
            }
        if ( o->pointer_motion(this, ev) )
            focused = o;
        return;
        }

    if ( focused )
        {
        focused->pointer_leave(this);
        focused = 0;
        }
    }

void CairoUI::pointer_enter ()
    {
    }

void CairoUI::pointer_leave ()
    {
    if ( focused )
        {
        focused->pointer_leave(this);
        focused = 0;
        }
    }

    // TODO To be uber correct, only set what the geometry originally had..
    // TODO see if there is any kind of standard for tracking a window
    //      position from right/bottom yet
void CairoUI::update_geometry_prefs (const GenericEvent *ev)
    {
    char buf[20];
    if ( prefs->is_set("geometry") )
        {
        if ( ev->x != x || ev->y != y ||
             ev->width != width || ev->height != height )
            {
            sprintf(buf, "%dx%d+%d+%d", ev->width, ev->height, ev->x, ev->y);
            prefs->set("geometry", buf);
            }
        }
    else if ( prefs->is_set("size") )
        {
        if ( ev->x != x || ev->y != y )
            {
            sprintf(buf, "%dx%d", width, height);
            prefs->set("size", buf);
            }
        }
    }

// This is assuredly stupid, but so far has resisted being refactored.
void CairoUI::first_configure (GenericEvent *ev)
    {
 //   bool moved_window = false;

    if ( prefs->is_set("geometry") )
        {
        const char *p;
        int orig_x, orig_y;
        int junk;
        if ( (p = prefs->get("geometry")) != 0 &&
             sscanf(p, "%dx%d+%d+%d", &junk, &junk, &orig_x, &orig_y) == 4 )
            {
            if ( orig_x != ev->x || orig_y != ev->y )
                {
                local->move(orig_x, orig_y);
//                moved_window = true;
                }
            }
        }

    width = ev->width;
    height = ev->height;
    x = ev->x;
    y = ev->y;

    local->resized(this);
    update_layout();
    // TODO  fire some Connecting... event
    redraw(true);
    if ( !mcp->connect() )
        {
        // TODO  fire some connect failure event (for display purposes)
        //       or possibly a fake disconnect
        redraw(true);
        }
    // else
        // do nothing here, the connect event has already been fired
    }

void CairoUI::configure (GenericEvent *ev)
    {
// fix the verbose log below if you remove this
mcp->log_verbose(12, "CairoUI.configure: %dx%d+%d+%d -> %dx%d+%d+%d\n",
                 x,y,width,height, ev->x,ev->y,ev->width,ev->height);
    update_geometry_prefs(ev);
    x = ev->x;
    y = ev->y;
    if ( ev->width != width || ev->height != height )
        {
        if ( mcp->verbose <= 11 )
            mcp->log_verbose(10, "CairoUI.configure: (%d,%d -> %d,%d)\n",
                             width, height, ev->x, ev->y);
        width = ev->width;
        height = ev->height;
        local->resized(this);
        update_layout();
        redraw(true);
        }
    }

// ---------------------------------------------------------------------- <fold<
//    -    Player events                                                  >fold>

void CairoUI::fire_event (Event ev)
    {
    for ( Widget *o = widget_list; o; o = o->next )
        o->event(this, ev);
    }

void CairoUI::fire_event (Event ev, EventData *event_data)
    {
    for ( Widget *o = widget_list; o; o = o->next )
        o->event(this, ev, event_data);
    }

void CairoUI::fire_event (WidgetEvent ev)
    {
    for ( Widget *o = widget_list; o; o = o->next )
        o->event(this, ev);
    }

void CairoUI::event (Event ev)
    {
    switch ( ev )
        {
        case EVENT_CONNECTED:
            mcp->maintain_playtime(true);
            playing_or_paused = false;
            update_layout();
            fire_event(ev);
            break;

        case EVENT_DISCONNECTED:
            local->set_title(default_window_title);
            playing_or_paused = false;
            update_layout();
            fire_event(ev);
            break;

        case EVENT_PLAYING:
        case EVENT_PAUSED:
            if ( !playing_or_paused )
                {
                playing_or_paused = true;
                update_layout();
                }
            fire_event(ev);
            break;

        case EVENT_STOPPED:
            local->set_title(default_window_title);
            playing_or_paused = false;
            update_layout();
            fire_event(ev);
            break;

        case EVENT_CURRENT_INFO:
            set_song_title();
            fire_event(ev);
            break;

        case EVENT_POSITION:
        case EVENT_PLAYTIME:
        case EVENT_PLAYLIST:
        case EVENT_VOLUME:
        case EVENT_PLAYLIST_INFO:
            fire_event(ev);
            break;

        default:
            break;
        }
    }

void CairoUI::event (Event ev, EventData *event_data)
    {
    switch ( ev )
        {
        case EVENT_PICTURE_DATA:
            fire_event(ev, event_data);
            break;

        default:
            break;
        }
    }

// ---------------------------------------------------------------------- <fold<
//    Icon                                                                >fold>

/*  Callers probably want to do this:
    cairo_translate(cr, x, y);
    cairo_scale(cr, width/100.0, height/100.0);
    draw_icon(cr, [...]);
*/
void CairoUI::draw_icon (cairo_t *cr, bool draw_mask, Color *fg, Color *bg)
    {
    // The Icon is plotted inside a 100x100 square.

  class _nestfunc // nested function workalike
    {
    /* The border of the icon is a (rounded) square.
     * The edge of that border is drawn between these 4 points:
     * (The rounded corners end up within the 100x100 area)
     * point A (50,-6)  B (-6,50)  C (50,106)  D (106,50)
     * AB: x = -y + 44       BC: x = y - 56
     * AD: x = y + 56        CD: x = -y + 156
     */
    public:
    const double BorderWidth;
    const double Ax, Ay, Bx, By, Cx, Cy, Dx, Dy,
                 flag_top_left_x,  flag_top_left_y,
                 flag_top_mid_x,   flag_top_mid_y,
                 flag_top_right_x, flag_top_right_y,
                 flag_bot_right_x, flag_bot_right_y,
                 flag_bot_left_x,  flag_bot_left_y,
                 notehead_x, notehead_y,
                 notehead_radius,
                 stem_bot_right_x, stem_bot_right_y;
    double       stem_bot_left_x, stem_bot_left_y,
                 corner_radius, corner_delta;

    // TODO  more of these values should be derived
    //       the flag arcs are a little dodgy
    //       need a shadow below and to the right of the note
    //       A shadow below the icon would also be nice for light backgrounds.
    _nestfunc ()
      : BorderWidth(7),
        Ax(50), Ay(-6),  Bx(-6), By(50),
        Cx(50), Cy(106),  Dx(106), Dy(50),

        // Define a few points to base the image on.
        flag_top_left_x(30),  flag_top_left_y(33),
        flag_top_mid_x(50),   flag_top_mid_y(20),
        flag_top_right_x(89), flag_top_right_y(33),  // on AD
        flag_bot_right_x(90), flag_bot_right_y(34),  // on AD
        flag_bot_left_x(44),  flag_bot_left_y(40),
        notehead_x(44), notehead_y(80),

        // Now comes a few derived points

        /* Finding the notehead radius:  The distance to the border is the
         * normal line at slope -1, from (44,80):   y = -x + 124
         * Substituting into BC: x = y - 56 to find the intercept point:
         * y = -(y - 56) + 124 : y = -y +180 : y=90, x=34
         * Distance is therefore: 44,80 to 34,90 so: sqrt(10^2 + 10^2)
         */
        notehead_radius(14.1421),
        // This point must make a tangent line for the right side of the stem
        // so that the stem is smooth with the notehead.
        // -- refigure if the notehead or flag_bot_left_xy change
        stem_bot_right_x(57.92), stem_bot_right_y(77.50)
        {
        stem_bot_left_x = stem_bot_right_x - notehead_radius/12,
        stem_bot_left_y = stem_bot_right_y;

        /* Determine the corner arc radius & arc-border line intercept points
         * using the same method as for the notehead.
         * The centers of each corner arc are at +20 or -20 from the edge.
         * Find where 50,20 intercepts AB.  y = x - 30, AB: x = -y + 44
         * x = -x + 74, x=37, y=7   from 50, -6  gives a delta of 13
         * Distance: 50,20 to 37,7  sqrt(13^2 + 13^2)
         */
        corner_delta = 13;  //  = 50 - ((30 + (50 + Ay)) / 2)
        corner_radius = 18.384776;  // = sqrt(corner_delta^2 + corner_delta^2)
        }

    void plot_border (cairo_t *cr)
        {
        #define deg(degree) ((double)(degree) * M_PI/180)
        cairo_move_to(cr, Ax - corner_delta, Ay + corner_delta);
        cairo_arc(cr, Ax, 20, corner_radius, deg(225), deg(315));
//        cairo_line_to(cr, Dx - corner_delta, Dy - corner_delta);
        cairo_arc(cr, 80, Dy, corner_radius, deg(315), deg(45));
//        cairo_line_to(cr, Cx + corner_delta, Cy - corner_delta);
        cairo_arc(cr, Cx, 80, corner_radius, deg(45), deg(135));
//        cairo_line_to(cr, Bx + corner_delta, By + corner_delta);
        cairo_arc(cr, 20, By, corner_radius, deg(135), deg(225));
        cairo_line_to(cr, Ax - corner_delta, Ay + corner_delta);
        #undef deg
        }

#if 0
    // snippet to see if the arcs end in the expected places.
    void border_debug (CairoUI *ui)
        {
        cairo_move_to(cairo, Ax - corner_delta, Ay + corner_delta);
         cairo_line_to(cairo, Ax + corner_delta, Ay + corner_delta);
        cairo_line_to(cairo, Dx - corner_delta, Dy - corner_delta);
         cairo_line_to(cairo, Dx - corner_delta, Dy + corner_delta);
        cairo_line_to(cairo, Cx + corner_delta, Cy - corner_delta);
         cairo_line_to(cairo, Cx - corner_delta, Cy - corner_delta);
        cairo_line_to(cairo, Bx + corner_delta, By + corner_delta);
         cairo_line_to(cairo, Bx + corner_delta, By - corner_delta);
        cairo_line_to(cairo, Ax - corner_delta, Ay + corner_delta);
        }
#endif

    // First plot the exterior border to fill in the interior.
    void layer1 (cairo_t *cr, Color *bg)
        {
        cairo_set_line_width(cr, 0);
        plot_border(cr);
        cairo_clip_preserve(cr);
        bg->set(cr);
        cairo_fill_preserve(cr);  // layer2 will fill again
        }

    // Make the interior more interesting with a circular gradient
    // (slightly lower than the center, tending towards dark on the edge)
    void layer2 (cairo_t *cr)
        {
        cairo_pattern_t *pattern;
        pattern = cairo_pattern_create_radial(50, 60, 0,  50, 60, 60);
        cairo_pattern_add_color_stop_rgba(pattern, 0.0, 0, 0, 0, 0);
        cairo_pattern_add_color_stop_rgba(pattern, 0.6, 0, 0, 0, .8);
        cairo_pattern_add_color_stop_rgba(pattern, 1.0, 0, 0, 0, .9);
        cairo_set_source(cr, pattern);
        cairo_fill(cr);
        cairo_pattern_destroy(pattern);
        }

    // Add the 'reflection' after clipping further inside the border.
    void layer3 (cairo_t *cr)
        {
        // Resize such that the clipping area is well inside the border
        const double Offset = 2*BorderWidth;
        // halfway between the top of the notehead and the middle
        const double bottom = ((notehead_y - notehead_radius) + 50) / 2;
//        const double top = 50 - (bottom-50) + 3;

        cairo_save(cr);
        cairo_translate(cr, Offset, Offset);
        cairo_scale(cr, (100.0-Offset*2)/100, (100.0-Offset*2)/100);
        cairo_set_line_width(cr, BorderWidth);
        plot_border(cr);
        cairo_clip(cr);

        // Fade the reflection gradually, then rapidly about halfway up
        cairo_pattern_t *pattern;
        pattern = cairo_pattern_create_linear(50, bottom, 50, 0);
        cairo_pattern_add_color_stop_rgba(pattern, 0.0, 1, 1, 1, .15);
        cairo_pattern_add_color_stop_rgba(pattern, 0.5, 1, 1, 1, .05);
        cairo_pattern_add_color_stop_rgba(pattern, 0.7, 1, 1, 1, 0);
        cairo_pattern_add_color_stop_rgba(pattern, 1.0, 1, 1, 1, 0);

        cairo_move_to(cr, 0, bottom);
        cairo_curve_to(cr, 40,10, 60,90, 100,45);
        cairo_line_to(cr, 100,0);
        cairo_line_to(cr, 0,0);
        cairo_line_to(cr, 0, (bottom + 50)/2);

        cairo_set_source(cr, pattern);
        cairo_fill(cr);
        cairo_pattern_destroy(pattern);

        cairo_restore(cr);
        }

    // Add a glow effect with a a subtle white gradient outside the border
    void layer4 (cairo_t *cr)
        {
        // use: cairo_pattern_create_radial(x1,y1,r1, x2,y2,r2)
        // to create a radial pattern for every corner and a linear
        // gradient for each side.
        }

    // Draw the white exterior border
    void layer5 (cairo_t *cr, Color *fg)
        {
        const double Offset = BorderWidth / 2;

        fg->set(cr);
        // Resize such that the plotted line is the outside edge
        cairo_save(cr);
        cairo_translate(cr, Offset, Offset);
        cairo_scale(cr, (100.0-Offset*2)/100, (100.0-Offset*2)/100);
        cairo_set_line_width(cr, BorderWidth);
        plot_border(cr);
        cairo_stroke(cr);
        cairo_restore(cr);
        }

    // Finally, draw the note in the middle
    void layer6 (cairo_t *cr)
        {
        cairo_set_line_width(cr, 0);

        cairo_arc(cr, notehead_x, notehead_y, notehead_radius, 0, 2*M_PI);
        cairo_fill(cr);

        // The flag was composed by trial and error for the most part.
        cairo_move_to(cr, stem_bot_left_x, stem_bot_left_y);
        cairo_line_to(cr, flag_top_left_x, flag_top_left_y);
        cairo_curve_to(cr, flag_top_left_x + 15, flag_top_left_y - 18,
                       flag_top_mid_x, flag_top_mid_y,
                       flag_top_mid_x + 8, flag_top_mid_y + 4);
        cairo_curve_to(cr, flag_top_mid_x + 16, flag_top_mid_y + 8,
                       flag_top_right_x - 15, flag_top_right_y + 5,
                       flag_top_right_x, flag_top_right_y);
        cairo_line_to(cr, flag_bot_right_x, flag_bot_right_y);
        cairo_curve_to(cr, flag_bot_right_x - 15, flag_bot_right_y + 13,
                       flag_bot_left_x + 20, flag_bot_left_y - 17,
                       flag_bot_left_x, flag_bot_left_y);
        cairo_line_to(cr, stem_bot_right_x, stem_bot_right_y);
        cairo_line_to(cr, stem_bot_left_x, stem_bot_left_y);

        cairo_fill(cr);
        }
    } nf;

    if ( draw_mask )  // for constructing pixmaps
        {
        cairo_set_line_width(cr, 0);
        cairo_set_source_rgb(cr, 0,0,0);
        cairo_rectangle(cr, 0,0, 100, 100);
        cairo_fill(cr);
        cairo_set_source_rgb(cr, 1,1,1);
        cairo_set_line_width(cr, 0);
        nf.plot_border(cr);
        cairo_fill(cr);
        }
    else
        {
        if ( !fg ) fg = &icon_fg;
        if ( !bg ) bg = &icon_bg;
        nf.layer1(cr, bg);
        nf.layer2(cr);
        nf.layer3(cr);
        nf.layer4(cr);
        nf.layer5(cr, fg);
        nf.layer6(cr);
        }
    }

// ---------------------------------------------------------------------- <fold<


} // namespace uic
